agora inbox for pgsql-hackers@postgresql.orghelp / color / mirror / Atom feed
[PATCH v14 6/8] Row pattern recognition patch (docs). 1133+ messages / 2 participants [nested] [flat]
* [PATCH v14 6/8] Row pattern recognition patch (docs). @ 2024-02-28 13:59 Tatsuo Ishii <ishii@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Tatsuo Ishii @ 2024-02-28 13:59 UTC (permalink / raw) --- doc/src/sgml/advanced.sgml | 80 ++++++++++++++++++++++++++++++++++++ doc/src/sgml/func.sgml | 54 ++++++++++++++++++++++++ doc/src/sgml/ref/select.sgml | 38 ++++++++++++++++- 3 files changed, 170 insertions(+), 2 deletions(-) diff --git a/doc/src/sgml/advanced.sgml b/doc/src/sgml/advanced.sgml index 755c9f1485..cf18dd887e 100644 --- a/doc/src/sgml/advanced.sgml +++ b/doc/src/sgml/advanced.sgml @@ -537,6 +537,86 @@ WHERE pos < 3; <literal>rank</literal> less than 3. </para> + <para> + Row pattern common syntax can be used to perform row pattern recognition + in a query. Row pattern common syntax includes two sub + clauses: <literal>DEFINE</literal> + and <literal>PATTERN</literal>. <literal>DEFINE</literal> defines + definition variables along with an expression. The expression must be a + logical expression, which means it must + return <literal>TRUE</literal>, <literal>FALSE</literal> + or <literal>NULL</literal>. The expression may comprise column references + and functions. Window functions, aggregate functions and subqueries are + not allowed. An example of <literal>DEFINE</literal> is as follows. + +<programlisting> +DEFINE + LOWPRICE AS price <= 100, + UP AS price > PREV(price), + DOWN AS price < PREV(price) +</programlisting> + + Note that <function>PREV</function> returns the price column in the + previous row if it's called in a context of row pattern recognition. So in + the second line the definition variable "UP" is <literal>TRUE</literal> + when the price column in the current row is greater than the price column + in the previous row. Likewise, "DOWN" is <literal>TRUE</literal> when when + the price column in the current row is lower than the price column in the + previous row. + </para> + <para> + Once <literal>DEFINE</literal> exists, <literal>PATTERN</literal> can be + used. <literal>PATTERN</literal> defines a sequence of rows that satisfies + certain conditions. For example following <literal>PATTERN</literal> + defines that a row starts with the condition "LOWPRICE", then one or more + rows satisfy "UP" and finally one or more rows satisfy "DOWN". Note that + "+" means one or more matches. Also you can use "*", which means zero or + more matches. If a sequence of rows which satisfies the PATTERN is found, + in the starting row of the sequence of rows all window functions and + aggregates are shown in the target list. Note that aggregations only look + into the matched rows, rather than whole frame. In the second or + subsequent rows all window functions and aggregates are NULL. For rows + that do not match the PATTERN, all window functions and aggregates are + shown AS NULL too, except count which shows 0. This is because the + unmatched rows are in an empty frame. Example of + a <literal>SELECT</literal> using the <literal>DEFINE</literal> + and <literal>PATTERN</literal> clause is as follows. + +<programlisting> +SELECT company, tdate, price, + first_value(price) OVER w, + max(price) OVER w, + count(price) OVER w +FROM stock, + WINDOW w AS ( + PARTITION BY company + ORDER BY tdate + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + INITIAL + PATTERN (LOWPRICE UP+ DOWN+) + DEFINE + LOWPRICE AS price <= 100, + UP AS price > PREV(price), + DOWN AS price < PREV(price) +); +</programlisting> +<screen> + company | tdate | price | first_value | max | count +----------+------------+-------+-------------+-----+------- + company1 | 2023-07-01 | 100 | 100 | 200 | 4 + company1 | 2023-07-02 | 200 | | | + company1 | 2023-07-03 | 150 | | | + company1 | 2023-07-04 | 140 | | | + company1 | 2023-07-05 | 150 | | | 0 + company1 | 2023-07-06 | 90 | 90 | 130 | 4 + company1 | 2023-07-07 | 110 | | | + company1 | 2023-07-08 | 130 | | | + company1 | 2023-07-09 | 120 | | | + company1 | 2023-07-10 | 130 | | | 0 +</screen> + </para> + <para> When a query involves multiple window functions, it is possible to write out each one with a separate <literal>OVER</literal> clause, but this is diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index e5fa82c161..8dd97501a7 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -22364,6 +22364,7 @@ SELECT count(*) FROM sometable; returns <literal>NULL</literal> if there is no such row. </para></entry> </row> + </tbody> </tgroup> </table> @@ -22403,6 +22404,59 @@ SELECT count(*) FROM sometable; Other frame specifications can be used to obtain other effects. </para> + <para> + Row pattern recognition navigation functions are listed in + <xref linkend="functions-rpr-navigation-table"/>. These functions + can be used to describe DEFINE clause of Row pattern recognition. + </para> + + <table id="functions-rpr-navigation-table"> + <title>Row Pattern Navigation Functions</title> + <tgroup cols="1"> + <thead> + <row> + <entry role="func_table_entry"><para role="func_signature"> + Function + </para> + <para> + Description + </para></entry> + </row> + </thead> + + <tbody> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>prev</primary> + </indexterm> + <function>prev</function> ( <parameter>value</parameter> <type>anyelement</type> ) + <returnvalue>anyelement</returnvalue> + </para> + <para> + Returns the column value at the previous row; + returns NULL if there is no previous row in the window frame. + </para></entry> + </row> + + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>next</primary> + </indexterm> + <function>next</function> ( <parameter>value</parameter> <type>anyelement</type> ) + <returnvalue>anyelement</returnvalue> + </para> + <para> + Returns the column value at the next row; + returns NULL if there is no next row in the window frame. + </para></entry> + </row> + + </tbody> + </tgroup> + </table> + <note> <para> The SQL standard defines a <literal>RESPECT NULLS</literal> or diff --git a/doc/src/sgml/ref/select.sgml b/doc/src/sgml/ref/select.sgml index 9917df7839..1575fc2167 100644 --- a/doc/src/sgml/ref/select.sgml +++ b/doc/src/sgml/ref/select.sgml @@ -969,8 +969,8 @@ WINDOW <replaceable class="parameter">window_name</replaceable> AS ( <replaceabl The <replaceable class="parameter">frame_clause</replaceable> can be one of <synopsis> -{ RANGE | ROWS | GROUPS } <replaceable>frame_start</replaceable> [ <replaceable>frame_exclusion</replaceable> ] -{ RANGE | ROWS | GROUPS } BETWEEN <replaceable>frame_start</replaceable> AND <replaceable>frame_end</replaceable> [ <replaceable>frame_exclusion</replaceable> ] +{ RANGE | ROWS | GROUPS } <replaceable>frame_start</replaceable> [ <replaceable>frame_exclusion</replaceable> ] [row_pattern_common_syntax] +{ RANGE | ROWS | GROUPS } BETWEEN <replaceable>frame_start</replaceable> AND <replaceable>frame_end</replaceable> [ <replaceable>frame_exclusion</replaceable> ] [row_pattern_common_syntax] </synopsis> where <replaceable>frame_start</replaceable> @@ -1077,6 +1077,40 @@ EXCLUDE NO OTHERS a given peer group will be in the frame or excluded from it. </para> + <para> + The + optional <replaceable class="parameter">row_pattern_common_syntax</replaceable> + defines the <firstterm>row pattern recognition condition</firstterm> for + this + window. <replaceable class="parameter">row_pattern_common_syntax</replaceable> + includes following subclauses. <literal>AFTER MATCH SKIP PAST LAST + ROW</literal> or <literal>AFTER MATCH SKIP TO NEXT ROW</literal> controls + how to proceed to next row position after a match + found. With <literal>AFTER MATCH SKIP PAST LAST ROW</literal> (the + default) next row position is next to the last row of previous match. On + the other hand, with <literal>AFTER MATCH SKIP TO NEXT ROW</literal> next + row position is always next to the last row of previous + match. <literal>DEFINE</literal> defines definition variables along with a + boolean expression. <literal>PATTERN</literal> defines a sequence of rows + that satisfies certain conditions using variables defined + in <literal>DEFINE</literal> clause. If the variable is not defined in + the <literal>DEFINE</literal> clause, it is implicitly assumed + following is defined in the <literal>DEFINE</literal> clause. + +<synopsis> +<literal>variable_name</literal> AS TRUE +</synopsis> + + Note that the maximu number of variables defined + in <literal>DEFINE</literal> clause is 26. + +<synopsis> +[ AFTER MATCH SKIP PAST LAST ROW | AFTER MATCH SKIP TO NEXT ROW ] +PATTERN <replaceable class="parameter">pattern_variable_name</replaceable>[+] [, ...] +DEFINE <replaceable class="parameter">definition_varible_name</replaceable> AS <replaceable class="parameter">expression</replaceable> [, ...] +</synopsis> + </para> + <para> The purpose of a <literal>WINDOW</literal> clause is to specify the behavior of <firstterm>window functions</firstterm> appearing in the query's -- 2.25.1 ----Next_Part(Thu_Feb_29_09_19_54_2024_640)-- Content-Type: Text/X-Patch; charset=us-ascii Content-Transfer-Encoding: 7bit Content-Disposition: inline; filename="v14-0007-Row-pattern-recognition-patch-tests.patch" ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <nathan@postgresql.org> 0 siblings, 0 replies; 1133+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1133+ messages in thread
end of thread, other threads:[~2026-05-28 20:42 UTC | newest] Thread overview: 1133+ messages (download: mbox mbox.gz follow: Atom feed) -- links below jump to the message on this page -- 2024-02-28 13:59 [PATCH v14 6/8] Row pattern recognition patch (docs). Tatsuo Ishii <ishii@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <nathan@postgresql.org>
This inbox is served by agora; see mirroring instructions for how to clone and mirror all data and code used for this inbox